// ==UserScript==
// @name           Vanilla Infinite Scroll
// @namespace      http://ngo.org.uk/nik/
// @description    Infinite scroll for pages on Vanilla forums
// @include        http://forum.stillwater-rock.com/*
// ==/UserScript==

var nextPageLink;		// The link to the next page to load


/**
 * Fetch the element that contains the list to modify -- either has the
 * id 'Discussions' or 'Comments' depending on the page we're on.
 */
function getDiscussionsOrComments(doc) {
    var e = doc.getElementById('Discussions');
    if (e) {
        return e;
    } else {
        return doc.getElementById('Comments');
    }
}


/**
 * Fetch the next page in to an iframe, and pull out the comments and
 * the address of the following page.  Insert the comments in to the
 * current page.
 *
 * The shenanigans around the onload event are to work around Firefox bugs,
 * see http://wiki.greasespot.net/HTML_injection_tips for more.
 *
 * When done calls the callback provided in cbComplete.
 */
function getNextPage(cbComplete) {
    GM_log('getNextPage(' + nextPageLink + ')');

    GM_xmlhttpRequest({
        method: 'GET',
	url: nextPageLink,
	onload: function(response) {
            var iframeOnload = function() {
                iframeContentDocument = iframe.contentDocument;
                iframeContentDocument.getElementsByTagName('body')[0].innerHTML = response.responseText;

                processNextPage(iframeContentDocument);
                cbComplete();
            };

            var iframe = createIframe(iframeOnload);
	    }
        });
}


/**
 * Get the entries from the next page, and insert them in to the
 * current page.
 */
function processNextPage(iframeContentDocument) {
    ol_doc    = getDiscussionsOrComments(document);
    ol_iframe = getDiscussionsOrComments(iframeContentDocument);
    
    // Record the next page to load (i.e., the page after the one we've
    // just pre-loaded)
    nextPageLink = getNextPageLink(iframeContentDocument);

    // Loop through entries on the iframe, appending to the main doc
    var entries = ol_iframe.childNodes;
    var entry;
    GM_log('Found ' + entries.length + ' entries');
    for (var i = 0, l = entries.length; i < l; i++) {
        entry = document.importNode(entries[i], true);
        ol_doc.appendChild(entry);
    }
}


/**
 * Create an iframe to load the next page in to.  The function provided
 * as cbOnload is set as the new iframe's 'onload' handler.
 */
function createIframe(cbOnload) {
    var iframe = document.createElement('iframe');

    iframe.addEventListener('load', cbOnload, false);
    iframe.src = 'about:blank';
    iframe.style.width = 0;
    iframe.style.height = 0;
    iframe.style.border = 0;

    document.getElementsByTagName('body')[0].appendChild(iframe);

    return iframe;
}


/**
 * Small state machine.  Check the current scroll state every checkEvery
 * ms.  If we should load then stick a note on the page, and initiate
 * the process of loading the next page and inserting the content.
 * Provide a callback that's called when that's done to remove the loading
 * note, figure out if there's another next page, and kick start the timer
 * again if so.
 */
function main(){
    var checkEvery = 100; // how frequently to check the scroll status (ms)
    var preloadDistance = 500; // start preloading this many px from bottom
    var isUpdating = false;
    var intervalTimer;
    var loadingNote;

    var startTimer = function() {
	intervalTimer = setInterval(checkAndLoad, checkEvery);
    }

    var checkAndLoad = function() {
	if (isUpdating == false && 
            getScrollPositionFromBottom() < preloadDistance) {
	    GM_log('scrollPosFromBottom: ' + getScrollPositionFromBottom());
            clearInterval(intervalTimer);
	    isUpdating = true;
	    showLoading();
	    getNextPage(doneLoading);
	}
    }

    var showLoading = function() {
	GM_log('showLoading');
	loadingNote = document.createElement('li');
	loadingNote.innerHTML = 'Loading...';
	getDiscussionsOrComments(document).appendChild(loadingNote);
    }

    var doneLoading = function() {
        GM_log('doneLoading');
	isUpdating = false;
        if (nextPageLink) {
            startTimer();
        }
	hideLoading();
    }
    
    var hideLoading = function() {
	GM_log('hideLoading');
	loadingNote.parentNode.removeChild(loadingNote);
    }

    nextPageLink = getNextPageLink(document);
    toInfinity();

    if(nextPageLink) {
	startTimer();
    }
}


/**
 * Figure out how far to go before the bottom of the page is reached.
 */
function getScrollPositionFromBottom() {
    return parseInt(document.body.offsetHeight) - parseInt(self.pageYOffset) - parseInt(self.innerHeight);
}


/**
 * Find the <a> element in the first <li> element following the <li>
 * element that has class="CurrentPage" in the <ol> that has 
 * class="PageList".
 *
 * Return the href, or null if there is no next page.
 */
function getNextPageLink(d) {
    GM_log('getNextPageLink()');

    var r = d.evaluate(
        "//ol[@class='PageList']/li[@class='CurrentPage']/following-sibling::li/a",
	d, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

    if (! r.snapshotLength) {
	GM_log('no next page');
	return;
    }

    var a = r.snapshotItem(0);
    GM_log(a.href);
    return a.href;
}


/**
 * Replace the current page / next pages indicator with an infinity
 * symbol.
 */
function toInfinity() {
    if (nextPageLink) {
        var r = document.evaluate(
            "//ol[@class='PageList']/li[@class='CurrentPage']",
            document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

        for (var i = 0, l = r.snapshotLength; i < l; i++) {
            r.snapshotItem(i).innerHTML = '&#8734';  // Infinity symbol
        }

        r = document.evaluate(
            "//ol[@class='PageList']/li[@class='CurrentPage']/following-sibling::li",
            document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

        for (var i = 0, l = r.snapshotLength; i < l; i++) {
            r.snapshotItem(i).parentNode.removeChild(r.snapshotItem(i));
        }
    }

    return;
}


main();